# Copyright (c) 2009, Benjamin Dumke
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice,
#    * this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#    * this list of conditions and the following disclaimer in the documentation
#    * and/or other materials provided with the distribution.
#    * Niether the name of the author nor the names of contributors may be used
#    * to endorse or promote products derived from this software without specific
#    *prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import sqlite3
import BaseHTTPServer as http
import urllib
import ro_url
import BeautifulSoup
import time

sites ={"so" : "stackoverflow.com",
        "meta" : "meta.stackoverflow.com",
        "sf" : "serverfault.com",
        "su" : "superuser.com"} # won't work during the beta

class HttpHandler(http.BaseHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        self.so = {}
        http.BaseHTTPRequestHandler.__init__(self,*args, **kwargs)

    def do_GET(self):
        print self.command
        print self.path
 
        data = self.path.strip("/").split("/")
        print self.server
        if data==['']:
            self.send_response(200)
            self.send_header("Content-type","text/html")
            self.end_headers()
            
            self.wfile.write("""<html><body>Please point your browser to http://localhost:port/site/userid
                    where port defaults to 8080, site is "so", "sf", "su", or "meta",
                    and userid is your user id number on that site.""")
            return
            
        
        if len(data) < 2 or data[0] not in sites or not data[1].isdigit():
            self.send_response(404)
            return
        
        message = ""
        if len(data) > 1 and data[-2] == "message":
            message = urllib.unquote(data[-1])
            data = data[:-2]

        sitecode = data[0]
        userid = data[1]
        data = data[2:] or ["show"]
        
        
        if (sitecode, userid) not in self.so:
           self.so[(sitecode, userid)] = StackOverflow(sitecode, userid)
           
        so = self.so[(sitecode, userid)]

        if data[0] == "updateall":
            so.update_posts()
            so.update_comments(10000)
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid)
        elif data[0] == "ignore":
            so.set_ignore_post(int(data[1]), True)
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid)
        elif data[0] == "unignore":
            so.set_ignore_post(int(data[1]), False)
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid+"/bump/"+data[1])
        elif data[0] == "bump":
            so.bump_post(int(data[1]))
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid+"/update")
        elif data[0] == "check":
                so.update_posts()
                self.send_response(303)
                self.send_header("Location","/"+sitecode+"/"+userid+"/update")
        elif data[0] == "update":
            try:
                so.update_comments(1)
                msg = ""
            except:
                msg = "/message/Error during updating"
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid+msg)
        elif data[0] == "acknowledge":
            so.acknowledge_post(int(data[1]))
            if len(data)>2:
                so.acknowledge_comment(int(data[2]))
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid)
        elif data[0] == "acknowledgeall":
            so.acknowledge_post()
            so.acknowledge_comment()
            self.send_response(303)
            self.send_header("Location","/"+sitecode+"/"+userid)
        elif data[0] == "show":
            self.send_response(200)
            self.send_header("Content-type","text/html")
            self.end_headers()
            
            self.wfile.write('<html><head><meta http-equiv="refresh" content="60; URL=\'/'+sitecode+'/'+str(userid)+'/update\'">\n<title>')
            if so.has_highlights():
                self.wfile.write('[!!] ')
            self.wfile.write(sitecode+" "+userid+'</title><style type="text/css">a{color:#300080}</style></head>\n')

            so.output_comments(self.wfile, message)
            self.wfile.write('</html>')
        
def run_server(port=8080):
    http.HTTPServer(('',port), HttpHandler).serve_forever()

class StackOverflow(object):
    def __init__(self, sitecode, userid):
        self.userid = int(userid)
        self.sitecode = sitecode
        self.site = sites.get(sitecode)
                     
        self.db = sqlite3.connect("comments_"+self.site+"_"+str(userid)+".db")
        self.cursor = self.db.cursor()

        self.db.execute("CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, score, postid, text, highlight, gone)")
        self.db.execute("CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, title, link, isanswer, lastupdate, ignore, lastcomment, highlight)")
        
        # we might be dealing with older database files that don't have all columns
        for col, val in [("gone", 0)]: # for the comments table
            try:
                self.cursor.execute("SELECT " + col + " FROM comments WHERE id=-1")
            except sqlite3.OperationalError:
                print "Creating new column", col, "in comments table"
                self.db.execute("ALTER TABLE comments ADD COLUMN " + col)
                self.db.execute("UPDATE comments SET " + col + " = ? WHERE 1", (val, ))
                
        for col, val in []: # for the posts table
            try:
                self.cursor.execute("SELECT " + col + " FROM posts WHERE id=-1")
            except sqlite3.OperationalError:
                print "Creating new column", col, "in posts table"
                self.db.execute("ALTER TABLE posts ADD COLUMN " + col)
                self.db.execute("UPDATE posts SET " + col + " = ? WHERE 1", (val, ))
        
        self.db.commit()
        
    def __del__(self):
        self.db.commit()
        self.db.close()
        
    def _get_comments_by_user_from_post(self, postid):
        """Returns last_commentid_on_this_post and a list of tuples (comment_id, comment_score, post_id, comment_text)"""

        u = ro_url.urlopen("http://" + self.site + "/posts/" + str(postid) + "/comments")
        soup = BeautifulSoup.BeautifulSoup(u)
        u.close()
        comment_id = -1
        ret = []
        for tr in soup.table.findAll("tr", "comment"):
            comment_id = int(tr["id"].split("-")[1])
            td = tr.find("td", "comment-text")
            text = ""
            thisuser = None
            for cont in td:
                if not isinstance(cont,basestring) and cont.get("class", "X").split()[0] == "comment-user":
                    try:
                        thisuser = int(cont["href"].split("/")[2])
                    except KeyError: # user is not registered
                        thisuser = -1
                    break
                text += cont.string or ""
            if thisuser == self.userid:
                text = text[:-14] # strip " &ndash;nbsp;"
                scoretd = tr.find("td", "comment-score")
                if scoretd is None:
                    score = 0
                else:
                    score = int(scoretd.span.string)
                ret.append((comment_id, score, postid, text))
        return comment_id, ret

    def _get_posts_with_comments_by_user(self):
        """Returns a dict {id : (question_title, post_link, post_is_answer)}
           of posts commented on by the user"""

        u = ro_url.urlopen("http://" + self.site + "/feeds/user/" + str(self.userid) + "/recent")
        soup = BeautifulSoup.BeautifulStoneSoup(u, selfClosingTags=['link'])
        u.close()

        ret = {}
        for entry in soup.findAll("entry"):
            summary = entry.summary.string
            if summary.startswith("comment "):
                link = entry.link['href']
                byhash = link.split("#")
                if len(byhash) > 1: # comment on answer
                    ret[int(byhash[1])] = (summary[8:], link, True)
                else: # comment on question
                    ret[int(int(byhash[0].split("/")[2]))] = (summary[8:], link, False)

        return ret
    
    def update_posts(self):
        print "Updating posts..."
        posts = self._get_posts_with_comments_by_user()
        for postid in posts:
            post = posts[postid]
            self.db.execute("INSERT OR IGNORE INTO posts VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (postid,) + post + (0, False, 0, 1) )
        self.db.commit()
            
    def update_comments(self,maxnum):
        posts = self.cursor.execute("SELECT * FROM posts WHERE NOT ignore ORDER BY lastupdate ASC LIMIT ?", (maxnum, )).fetchall()
        for i, post in enumerate(posts):
            print "Getting comments for post", post[0]
            lastcommentid, comments = self._get_comments_by_user_from_post(post[0])
            post_highlight = post[7] or (lastcommentid != post[6])
            print "Found", len(comments), "comments on post", post[0]
            
            for comment in comments:
                comment_highlight, oldscore, gone= self.cursor.execute("SELECT highlight, score, gone FROM comments WHERE id = ?", (comment[0],)).fetchone() or (0, 0, 0)
                comment_highlight = comment_highlight or (oldscore != comment[1])
                self.db.execute("INSERT OR REPLACE INTO comments VALUES (?, ?, ?, ?, ?, ?)", comment + (comment_highlight, gone) )
                
            archived_comments = [c[0] for c in self.cursor.execute("SELECT id FROM comments WHERE postid = ?", (post[0],)).fetchall()]
            found_comments = [c[0] for c in comments]
            
            for gone_comment in set(archived_comments) - set(found_comments):
                self.db.execute("UPDATE comments SET highlight = 1, gone = 1 WHERE id = ? AND gone = 0", (gone_comment,))
                print "gone:",gone_comment
                
            self.db.execute("UPDATE posts SET lastupdate = ?, lastcomment = ?, highlight = ? WHERE id = ?", (time.time(), lastcommentid, post_highlight, post[0]))
            if i+1 < len(posts):
                time.sleep(2.1)
        self.db.commit()
    
    def set_ignore_post(self, post_id, val):
        self.db.execute("UPDATE posts SET ignore=? WHERE id=?", (val, post_id))
        self.db.commit()

    def bump_post(self, post_id):
        self.db.execute("UPDATE posts SET lastupdate = -1 WHERE id=?", (post_id,))
        self.db.commit()
        
    def post_count(self):
        return self.cursor.execute("SELECT COUNT(id) FROM posts WHERE NOT ignore").fetchone()[0]
    
    def acknowledge_post(self, post_id = None):
        if post_id is not None:
            self.db.execute("UPDATE posts SET highlight=0 WHERE id=?", (post_id,))
        else:
            self.db.execute("UPDATE posts SET highlight=0 WHERE 1")
        self.db.commit()

    def acknowledge_comment(self, com_id = None):
        if com_id is not None:
            self.db.execute("UPDATE comments SET highlight=0 WHERE id=?", (com_id,))
        else:
            self.db.execute("UPDATE comments SET highlight=0 WHERE 1")
        self.db.commit()
        
    def has_highlights(self):
        ph = len(self.cursor.execute("SELECT id FROM posts WHERE highlight").fetchall()) > 0
        ch = len(self.cursor.execute("SELECT id FROM comments WHERE highlight").fetchall()) > 0
        return ph or ch
        
    def output_comments(self,f, message = ""):
        f.write("<body style='font-family: sans-serif;'>")
        if message:
            f.write("<span style = 'color: #c00000'>"+message+"</span><br>")
        f.write("Currently monitoring "+str(self.post_count())+" posts.")
        f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/check'>check for new</a>]")
        f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/updateall'>full update</a>]")
        if self.has_highlights():
            f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/acknowledgeall'>acknowledge all</a>]")
        f.write("<br><br>\n<table width=600px border=0 cols=2>\n")

        query = "SELECT score, text, title, link, isanswer, posts.id, posts.highlight, ignore, "\
                "(SELECT COUNT(id) FROM comments AS c2 WHERE c2.postid = comments.postid) AS siblings, comments.highlight, "\
                "comments.id, comments.gone FROM comments LEFT JOIN "\
                "posts ON comments.postid = posts.id  ORDER BY ignore ASC, (comments.highlight OR posts.highlight) DESC, score DESC, comments.id DESC"

        for com in self.cursor.execute(query).fetchall():
            col = ""
            scorecol = ""
            if com[7]: #ignore
                scorecol = col = " color: #a0a0a0;"
            if com[6]: #highlight post
                col=" color: #c00000;"
            if com[9]: #highlight score
                scorecol=" color: #c00000;"
                
            com = map(lambda s: s.encode("ascii","ignore") if isinstance(s,unicode) else s, com)
            f.write("<tr><td valign='top' style='"+scorecol+"'><strong>" + str(com[0]) + "</strong></td><td style='font-size: small;"+col+"'>")
            f.write(("<strike>" if com[11] else "") + str(com[1]) + ("</strike>" if com[11] else "") + "</td></tr>\n")
            f.write("<tr><td> </td><td style='font-size: x-small;'>(On <a href='http://" + self.site + com[3] + "'>" + (" an answer to " if com[4] else ""))
            f.write(com[2] + "</a>")

            sib = ""
            if com[8] > 1:
                sib = " "+str(com[8])+" comments"
            if com[7]:
                f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/unignore/"+str(com[5])+"'>unignore"+sib+"</a>]")
            else:
                f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/ignore/"+str(com[5])+"'>ignore"+sib+"</a>]")
                f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/bump/"+str(com[5])+"'>update</a>]")
            if com[6] or com[9]:
                f.write(" [<a href='/"+self.sitecode+"/"+str(self.userid)+"/acknowledge/"+str(com[5]))
                if com[9]:
                    f.write("/"+str(com[10]))
                f.write("'>acknowledge</a>]")
            f.write(")<br><br></td></tr>\n")

        f.write("</table></body>")

if __name__ == "__main__":
    from sys import argv
    if len(argv) > 1:
        run_server(int(argv[1]))
    else:
        run_server()
